//
// Created by glyn on 2024/3/15.
//
#pragma once

#include "basedomainsocket.hpp"

#include <string.h>
#include <thread>

template <uint16_t BUFFER_SIZE = AS_DOMAIN_BUFFER_SIZE>
class DgramDomainSocket : public BaseDomainSocket
{
public:
    std::function<void(std::string, std::string)> onMessageReceived;
    std::function<void(const char*, ssize_t, std::string)> onRawMessageReceived;

    explicit DgramDomainSocket(bool useConnect = false, FDR_ON_ERROR, int socketId = -1): BaseDomainSocket(onError, UDP, LOCALM,socketId)
    {
        if (useConnect)
        {
            std::thread t(Receive, this); // usage with Connect()
            t.detach();
        }
        else
        {
            std::thread t(ReceiveFrom, this);
            t.detach();
        }
    }
    
    // Send raw bytes to a spesific `host` & `port` with no connection
    ssize_t SendTo(const char* bytes, size_t byteslength, const char* path, FDR_ON_ERROR)
    {
        sockaddr_un pathAddr;


        pathAddr.sun_family = LOCALM;
        strcpy(pathAddr.sun_path, path);
        
        ssize_t sent_length = sendto(this->sock, bytes, byteslength, 0, (sockaddr*)&pathAddr, sizeof(pathAddr));
        if (sent_length == -1)
        {
            onError(errno, "Cannot send message to the address.");
            return -1;
        }

        return sent_length;
    }
    // Send raw bytes to a spesific `host` & `port` with no connection
    ssize_t SendTo(const char* bytes, size_t byteslength, const std::string& path, FDR_ON_ERROR)
    {
        return this->SendTo(bytes, byteslength, path.c_str(), onError);
    }

    // Send std::string to a spesific `host` & `port` with no connection
    ssize_t SendTo(const std::string& message, const char* path,  FDR_ON_ERROR)
    {
        return this->SendTo(message.c_str(), message.length(), path,  onError);
    }

    // Send std::string to a spesific `host` & `port` with no connection
    ssize_t SendTo(const std::string& message, const std::string& path, FDR_ON_ERROR)
    {
        return this->SendTo(message.c_str(), message.length(), path.c_str(), onError);
    }

    // Send raw bytes to the `Connect()`ed server.
    ssize_t Send(const char* bytes, size_t byteslength) { return send(this->sock, bytes, byteslength, 0); }
    // Send std::string to the `Connect()`ed server.
    ssize_t Send(const std::string& message) { return this->Send(message.c_str(), message.length()); }


    // Connect to a server with raw `uint32_t ipv4` and `uint16_t port` values.
    void Connect(const char* path, FDR_ON_ERROR)
    {
        this->address.sun_family = LOCALM;

        strcpy(this->address.sun_path,path);

        // Try to connect.
        int status = connect(this->sock, (const sockaddr* )&this->address, sizeof(sockaddr_un));
        if (status == -1)
        {
            onError(errno, "Connection failed to the host.");
            return;
        }
    }

    // Connect to a server with `host` address and `port` values.
    void Connect(const std::string& path, FDR_ON_ERROR) { this->Connect(path.c_str(), onError); }



private:
    static void Receive(DgramDomainSocket* udpSocket)
    {
        char tempBuffer[BUFFER_SIZE+1];
        ssize_t messageLength;

        while ((messageLength = recv(udpSocket->sock, tempBuffer, BUFFER_SIZE, 0)) != -1)
        {
            tempBuffer[messageLength] = '\0';
            if (udpSocket->onMessageReceived)
                udpSocket->onMessageReceived(std::string(tempBuffer, messageLength), pathToString(udpSocket->address));

            if (udpSocket->onRawMessageReceived)
                udpSocket->onRawMessageReceived(tempBuffer, messageLength, pathToString(udpSocket->address));
        }
    }

    static void ReceiveFrom(DgramDomainSocket* udpSocket)
    {
        sockaddr_un pathAddr;
        socklen_t pathAddrSize = sizeof(pathAddr);

        char tempBuffer[BUFFER_SIZE+1];
        ssize_t messageLength;

        while ((messageLength = recvfrom(udpSocket->sock, tempBuffer, BUFFER_SIZE, 0, (sockaddr* )&pathAddr, &pathAddrSize)) != -1)
        {
            tempBuffer[messageLength] = '\0';
            if (udpSocket->onMessageReceived)
                udpSocket->onMessageReceived(std::string(tempBuffer, messageLength), pathToString(pathAddr));

            if (udpSocket->onRawMessageReceived)
                udpSocket->onRawMessageReceived(tempBuffer, messageLength, pathToString(pathAddr));
        }
    }
};